/*
 * Copyright 2011-2012 by The Regents of the University of California Licensed
 * under the Apache License, Version 2.0 (the "License"); you may not use this
 * file except in compliance with the License. you may obtain a copy of the
 * License from
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package edu.ucsd.db.bassarisk.data.compression;

import java.nio.ByteBuffer;

import edu.ucsd.db.bassarisk.data.api.IValueType.IValueReference;
import edu.ucsd.db.bassarisk.data.values.LongType;
import edu.ucsd.db.bassarisk.data.values.LongType.LongTools;
import edu.ucsd.db.bassarisk.data.values.LongType.LongValueCodec;

/**
 * Variable length delta encodings have 1, 2, 3, 5 or 9 bytes.
 * 
 * <ul>
 * <li>0xxxxxxx; delta value is xxxxxxx
 * <li>100xxxxx; delta value (up to 2**13) is (xxxxx<<08)|(next byte)
 * <li>101xxxxx; delta value (up to 2**21) is (xxxxx<<16)|(next short)
 * <li>110xxxxx; delta value (up to 2**37) is (xxxxx<<32)|(next int)
 * <li>111_____; delta value (up to 2**64) is following 8 bytes
 * </ul>
 */
public class VariableLengthUnsignedLongSequenceCodec {
  public static final VariableLengthUnsignedLongSequenceCodec INSTANCE =
      new VariableLengthUnsignedLongSequenceCodec();

  private final long max1ByteDelta = 128L; // 7 bits
  private final long max2ByteDelta = 8192L; // 13 bits
  private final long max3ByteDelta = 2097152L; // 21 bits
  private final long max5ByteDelta = 137438953472L; // 38 bits

  private final LongValueCodec valCodec = LongTools.INSTANCE.getGenericCodec();

  public int bytesRequired(final long value) {
    assert value >= 0;

    if (value < this.max1ByteDelta) {
      return 1;
    } else if (value < this.max2ByteDelta) {
      return 2;
    } else if (value < this.max3ByteDelta) {
      return 3;
    } else if (value < this.max5ByteDelta) {
      return 5;
    } else {
      return 9;
    }
  }

  public int
      decode(final ByteBuffer buffer, final IValueReference<LongType> outValue) {
    int position = buffer.position();
    int bytesConsumed = this.decode(buffer, outValue, position, buffer.limit());
    buffer.position(position + bytesConsumed);
    return bytesConsumed;
  }

  public int decode(final ByteBuffer buffer,
                    final IValueReference<LongType> outValue, final int position,
                    final int limit) {
    // compute the number of bytes available in the buffer so we can detect
    // overflows
    int avalible = limit - position;

    byte header = buffer.get(position);

    if ((header & 0x80) == 0) {
      assert avalible >= 1;
      this.valCodec.setLongValue(header & 0x7F, outValue);
      return 1;
    } else {
      byte code = (byte) (header & (byte) 0xE0);
      long value = header & 0x1F;
      switch (code) {
      case ((byte) 0x80):
        assert avalible >= 2;
        value = value << 8;
        value |= (255L & buffer.get(position + 1));
        this.valCodec.setLongValue(value, outValue);
        return 2;
      case ((byte) 0xA0):
        assert avalible >= 3;
        value = value << 16;
        value |= (65535L & buffer.getShort(position + 1));
        this.valCodec.setLongValue(value, outValue);
        return 3;
      case ((byte) 0xC0):
        assert avalible >= 5;
        value = value << 32;
        value |= (4294967295L & buffer.getInt(position + 1));
        this.valCodec.setLongValue(value, outValue);
        return 5;
      case ((byte) 0xE0):
        assert value == 0;
        assert avalible >= 9;
        value = buffer.getLong(position + 1);
        this.valCodec.setLongValue(value, outValue);
        return 9;
      default:
        throw new RuntimeException("Unexpected delta code.");
      }
    }
  }

  public int encode(final ByteBuffer buffer, final long value) {
    int position = buffer.position();
    int bytesConsumed = this.encode(buffer, value, position, buffer.limit());
    buffer.position(position + bytesConsumed);
    return bytesConsumed;
  }

  public int encode(final ByteBuffer buffer, final long value, final int position,
                    final int limit) {
    assert value >= 0;
    // compute the number of bytes available in the buffer so we can detect
    // overflows
    int avalible = limit - position;

    if (value < this.max1ByteDelta) {
      if (avalible == 0) {
        return -1;
      } else {
        buffer.put(position, (byte) value);
        return 1;
      }
    } else if (value < this.max2ByteDelta) {
      if (avalible < 2) {
        return -1;
      } else {
        byte extend = (byte) value;
        byte head = (byte) (((value >> 8)) | 0x80);
        buffer.put(position, head).put(position + 1, extend);
        return 2;
      }
    } else if (value < this.max3ByteDelta) {
      if (avalible < 3) {
        return -1;
      } else {
        short extend = (short) value;
        byte head = (byte) (((value >> 16)) | 0xA0);
        buffer.put(position, head).putShort(position + 1, extend);
        return 3;
      }
    } else if (value < this.max5ByteDelta) {
      if (avalible < 5) {
        return -1;
      } else {
        int extend = (int) value;
        byte head = (byte) (((value >> 32)) | 0xC0);
        buffer.put(position, head).putInt(position + 1, extend);
        return 5;
      }
    } else {
      if (avalible < 9) {
        return -1;
      } else {
        buffer.put(position, (byte) 0xE0).putLong(position + 1, value);
        return 9;
      }
    }
  }

  public int skip(final ByteBuffer buffer, final int numberToSkip,
                  final int startPosition, final int limit) {
    int currentPosition = startPosition;

    for (int numberSkipped = 0; numberSkipped <= numberToSkip; ++numberSkipped) {
      assert currentPosition < limit;
      byte header = buffer.get(currentPosition);
      if ((header & 0x80) == 0) {
        currentPosition++;
      } else {
        byte code = (byte) (header & (byte) 0xE0);
        switch (code) {
        case ((byte) 0x80):
          currentPosition += 2;
        case ((byte) 0xA0):
          currentPosition += 3;
        case ((byte) 0xC0):
          currentPosition += 5;
        case ((byte) 0xE0):
          currentPosition += 9;
        default:
          throw new RuntimeException("Unexpected delta code.");
        }
      }
    }

    return currentPosition - startPosition;
  }
}
